Odpowiedzią na ograniczenia zwykłych tablic jest użycie baz danych, które z założenia przechowują informacje niezależnie od aplikacji, a ich zasoby nie są ulotne.
Ponadto każdy silnik bazy danych oferuje interfejs, dzięki któremu można łatwo pobierać, wyszukiwać czy modyfikować informacje. Nie mówimy tutaj o zwykłym "przechodzeniu" po liście obiektów, tylko o gotowych metodach, które najczęściej bardzo dużo robią za nas.
MongoDB jest jednym z wielu silników dostępnych na rynku, wyróżnia się jednak konstrukcją oraz sposobem przechowywania danych. Komunikacja z bazą bardzo przypomina to, co znamy już z JS-a (niedługo się o tym przekonasz). W dodatku same dane są przechowywane w formacie podobnym do znanego nam już JSON-a (tzw. BSON). Powoduje to, że MongoDB jest idealnym wyborem w przypadku aplikacji pisanych w Node.js. Potrafi dobrze z nimi współpracować, a z racji podobnej budowy, jest stosunkowo łatwy w nauce.
Przy okazji – nie bez powodu napisaliśmy "Node.js", a nie "JS". Wiesz już po poprzednich modułach, że klient nie komunikuje się z bazą danych. To rola serwera, który tworzymy w końcu z wykorzystaniem Node.js (nie zapominaj, że Express jest tylko frameworkiem).
Alternatywny wybór
Jeśli tematyka baz danych nie jest Ci obca, to bardzo możliwe, że było Ci dane pracować z dwoma innymi popularnymi technologiami – SQL lub MySQL. Moglibyśmy wykorzystać je zamiast MongoDB, jednak z racji tego, że dane są w nich przechowywane w trochę inny sposób, a sam interfejs do komunikacji to kompletnie nowa idea (język deklaratywny), byłoby to o wiele mniej wygodne. Nawet sama podstawowa konfiguracja zajęłaby nam o wiele więcej czasu.
Podsumujmy, do czego przyda się MongoDB?
Po pierwsze, da nam możliwość przechowywania dużej ilości danych w nieulotny sposób, więc restart aplikacji nie będzie oznaczać utraty wprowadzonych zmian. Do tego dane będą przechowywane niezależnie od aplikacji. Owszem, będzie miała do nich dostęp i możliwość modyfikacji, ale jej wyłączenie lub ponownie uruchomienie nie wpłynie na spójność naszych danych.
Po drugie, MongoDB oferuje nam również przyjazny interfejs do komunikacji. Oznacza to, że wyszukiwanie danych czy ich modyfikacja będą możliwe przy użyciu prostych komend. Usunięcie osób z kolekcji persons, których wiek jest równy 18 lat, wyglądałoby tak: db.persons.remove({ age: 18 }) – tylko jedna komenda. W przypadku tablic, jak zapewne pamiętasz, tych linijek byłoby 5.
Brzmi dobrze? Już wkrótce przekonasz się, że w praktyce również nie powinno sprawić Ci to problemu.
Tak naprawdę możemy traktować MongoDB jako bardziej zaawansowaną i niezależną alternatywę dla zwykłych tablic.
Pojawia się tylko jedno pytanie. Powiedzieliśmy o kilku zaletach MongoDB, ale nie rozwiązują one wszystkich problemów, o których była mowa we wprowadzeniu. Nie wspomnieliśmy, czy zaoferuje nam możliwość narzucania z góry struktury danych. Nie zająknęliśmy się również o relacyjności. Dlaczego? Dlatego, że akurat takich funkcjonalności samo MongoDB nie oferuje. Do tego będzie nam jeszcze potrzebna dodatkowa biblioteka – Mongoose. Na razie nie zawracaj sobie jednak tym głowy. Mongoose zajmiemy się później, teraz czas na poznanie MongoDB.
Instalacja MongoDB
Zaczniemy od instalacji. Wejdź na stronę mongodb.com i wybierz zakładkę "Server", następnie wskaż wersję 4.2.0 oraz swój system i kliknij przycisk "Download". Możesz wybrać nowszą edycję, jeśli w momencie czytania tego materiału będzie dostępna, aczkolwiek wszystkie snippety z kodem, które pojawią się niniejszym module, były sprawdzone w wersji 4.2.0. Zalecamy więc użycie właśnie jej.
MongoDB Compass
Wraz z MongoDB domyślnie pobierany jest również MongoDB Compass. To przyjazna aplikacja, która pozwala na przeglądanie baz danych za pomocą graficznego interfejsu. Opowiemy o niej trochę więcej później.
Uwaga!
Możesz natrafić na dwa problemy.
Pierwszy z dokończeniem instalacji. Proces czasami potrafi zatrzymać się na punkcie "Installing Compass". W takiej sytuacji po prostu przerwij działanie instalatora (sam MongoDB powinien być już w tym momencie zainstalowany) i pobierz Compass indywidualnie. Możesz to zrobić pod tym linkiem. Wybierz koniecznie Community Edition Stable.
Drugi z brakiem katalogu data/db. MongoDB chce przechowywać dane w folderze /data/db, który powinien znajdować w katalogu domowym (np. C:\data\db dla Windowsa). Musisz go więc utworzyć, aby MongoDB był w stanie uruchomić serwer bazy danych.
Pamiętaj, że katalog /data/db musi być stworzony w głównym katalogu systemu (root). Konieczne może być więc skorzystanie z komendy sudo przy tworzeniu folderu oraz nadanie mu potem odpowiednich uprawnień (za pomocą komendy chown). Muszą to być na tyle duże uprawnienia, aby MongoDB mogło odczytywać, zapisywać, modyfikować i usuwać dane z tego folderu. W przypadku Maca alternatywą może być pobranie całego pakietu za pomocą Homebrew.
Dostęp do MongoDB
Aby dodać nową bazę albo wyszukiwać i modyfikować dane, musimy w jakiś sposób komunikować się z MongoDB. Możemy to robić na kilka sposobów.
- Graficzny interfejs – mowa tu np. o wcześniej wspomnianym Compassie, który umożliwia łatwą modyfikację bazy danych.
- Node.js – wiemy, że serwery komunikują się z bazami danych, zatem jest to możliwe z poziomu aplikacji Node.js.
- Konsola – wraz z samym MongoDB na naszym komputerze instaluje się prosta aplikacja konsolowa (tzw. Mongo Shell), która pozwala na bezpośrednią komunikację z bazami danych za pomocą prostych komend.
Pierwszy sposób pełni głównie rolę kontrolną. Przyda nam się, gdy będziemy np. chcieli sprawdzić, czy nasza aplikacja Node.js poprawnie zapisuje dane w bazie, albo czy dobrze je zmodyfikowała.
Jak się domyślasz, najczęściej sięgniemy po drugi sposób. W końcu po to nam bazy danych – mają być zbiornikiem informacji dla naszych zaawansowanych aplikacji. Chcemy sprawdzić ilość wolnych miejsc na koncercie albo zrobić rezerwację? Serwer połączy się z bazą danych, pobierze informacje i je nam przekaże, albo zrobi odpowiedni wpis w bazie.
Sposób z konsolą raczej nie będzie przez nas wykorzystywany w normalnej pracy. Użyjemy go jednak teraz, aby pokazać Ci, jeszcze bez aplikacji, jakie komendy udostępnia MongoDB w celu komunikacji z bazą danych.
Tworzymy nową bazę
Najczęściej będziemy porozumiewać się z bazą danych z poziomu samego serwera naszej aplikacji. Na razie jednak poznamy MongoDB, komunikując się z nim przez konsolę.
Odnajdź teraz folder, w którym zainstalowano MongoDB, a w nim katalog bin oraz dwa pliki: mongod.exe i mongo.exe.
Zadaniem mongod.exe jest uruchomienie serwera bazy danych i powinien on włączać się automatycznie wraz ze startem systemu. Gdy działa, możemy komunikować się z bazą z poziomu aplikacji, Compassa, czy też po prostu konsoli. To ważne, zawsze kiedy chcesz łączyć się z lokalną bazą danych, ten proces musi być uruchomiony. Gdy pojawi się w przyszłości problem przy połączeniu, to upewnij się, że ten proces jest włączony – jeśli nie, wystartuj go ręcznie.
Teraz uruchom drugi plik, mongo.exe, czyli konsolową aplikację do komunikacji z bazami danych MongoDB.
Zacznijmy od sprawdzenia, jakie zasoby aktualnie znajdują się na naszym dysku, warto wiedzieć bowiem, że na jednym komputerze możemy przechowywać więcej niż jedną bazę danych. To istotne, bo przecież każda aplikacja może mieć swoją własną. Na przykład nasza witryna festiwalu mogłaby korzystać z bazy o nazwie NewWaveApp i posiadać dwie kolekcje – seats i concerts, a aplikacja listy zadań – columns i cards.
Komenda, która wskaże nam aktualnie dostępne bazy to:
show dbs
Pewnie spodziewasz się, że dostaniemy komunikat o braku jakiejkolwiek bazy danych. Nic bardziej mylnego. Do zapisywania informacji o swojej konfiguracji, użytkownikach bazy i hasłach, MongoDB używa... baz danych, stąd od razu lista trzech istniejących – admin, config oraz local. Nie musimy się nimi przejmować. Po prostu pamiętaj, że nie są one Twoim wytworem, a MongoDB wykorzystuje je do zapisywania własnych danych.
Teraz czas na dodanie nowej, testowej bazy danych. Nazwiemy ją companyDB. Powiedzmy, że będzie ona zawierała informacje o jakiejś firmie i pomieści kolekcje na temat pracowników, stanowisk, działów itd.
Co ciekawe, w MongoDB nie istnieje komenda do samego tworzenia nowej bazy danych. Zamiast tego używamy:
use <db-name>
Polecenie to służy do wskazania bazy danych, na której aktualnie chcemy działać, ale jednocześnie potrafi utworzyć nową, jeśli jeszcze jej nie ma. Gdy baza już istnieje, zostanie wybrana i udostępniona nam w obiekcie db.
Wpisz teraz następującą komendę w konsoli (mongo.exe):
use companyDB
Skoro jeszcze takiej bazy danych nie ma, MongoDB ją utworzy, a następnie wybierze (stąd też komunikat "Switched to...").
Od razu możemy przygotować również kolekcję danych. Służy do tego metoda createCollection, która wygląda tak:
db.createCollection(<collection-name>)
My wygenerujemy trzy kolekcje danych employees, deparatments i products. Utworzenie pierwszej z nich nastąpi dzięki komendzie:
db.createCollection('employees')
Możesz to potraktować jak rozkaz: do aktualnie wybranej bazy danych (db = companyDB) dodaj nową kolekcję o nazwie employees.
Analogicznie dodaj też nowe kolekcje departments i products.
Upewnij się
Aby sprawdzić, czy kolekcje faktycznie zostały dodane poprawnie, możesz skorzystać z komendy show collections. Pokaże Ci ona wszystkie kolekcje dostępne w aktualnie wybranej bazie.
Nasza baza danych companyDB ma w tej chwili następujący układ:
Jak zapewne udało Ci się zauważyć na grafice – aktualne dane z każdej kolekcji to puste tablice. Wspomnieliśmy już wcześniej, że MongoDB został zbudowany z myślą o komforcie developerów zaznajomionych z JS-em oraz formatem JSON. Kolekcje są więc zapisane jako tablice, a same konkretne elementy (w MongoDB nazywane dokumentami) w formacie BSON, który jest bardzo podobny do znanego Ci już JSON-a.
Tak naprawdę niewiele zmieni się w naszym podejściu do pracy z danymi, teraz będą już jednak nieulotne, przechowywane niezależnie od aplikacji i dostaniemy się do nich za pomocą prostych komend.
Operacje CRUD
Podstawą każdego silnika bazy danych jest oferowanie interfejsu komunikacyjnego, który pozwoli na kontakt z bazą i jej modyfikację. MongoDB daje nam takie możliwości przy użyciu zwykłych komend. Kilka z nich udało Ci się już poznać. Wiesz, że show dbs pokazuje listę baz danych na komputerze, use <db-name> pozwala wybrać bazę, na której chcemy aktualnie pracować, a db.createCollection utworzy nową kolekcję ("kolekcja" odnosi się po prostu do tablicy danych).
Oprócz tego każdy silnik musi oferować obowiązkowo zestaw poleceń do modyfikacji samych danych. W końcu, jaki sens miałaby baza danych, w której nie można nic zmieniać? Zestaw komend do wprowadzania, usuwania, modyfikacji oraz przeszukiwania określamy mianem CRUD.
CRUD to po prostu akronim od:
- Create – baza danych musi oferować możliwość dodawania nowych danych,
- Read – musi mieć także możliwość ich odczytu,
- Update – niezbędna jest również opcja aktualizacji,
- Delete – koniecznie musimy mieć też możliwość usuwania danych.
Oczywiście każdy silnik bazy danych może oferować o wiele więcej możliwości, ale operacje typu CRUD to absolutna podstawa. Bez tych czterech funkcjonalności obsługa bazy danych byłaby zwyczajnie niemożliwa, dlatego teraz właśnie nimi się zajmiemy.
Create
Zaczniemy od tworzenia danych.
MongoDB oferuje nam od razu dwie możliwości:
insertOne – służy do wstawienia jednego nowego dokumentu (elementu) do wybranej kolekcji (tablicy),
insertMany – pozwala na wstawienie wielu dokumentów do kolekcji.
Każdej z metod zestawu CRUD będziemy używać w następujący sposób:
db.<collection-name>.<method>
czyli np.
db.employees.insertOne({ firstName: 'John', lastName: 'Doe' });
Przyznaj, że to dość intuicyjny zapis. Wybieramy kolekcję i mówimy, co chcemy z nią zrobić. Brzmi to niczym rozkaz: w aktualnie wybranej bazie danych odnajdź kolekcję employees i wstaw do niej nowy dokument ({ firstName: 'John', lastName: 'Doe' }).
Jak widzisz, insertOne przyjmuje po prostu jeden obiekt i wprowadza go do danej kolekcji. W przypadku insertMany jako argument przekazywalibyśmy tablicę obiektów do wprowadzenia. Na przykład:
db.employees.insertMany([{ firstName: 'John', lastName: 'Doe'}, { firstName: 'Amanda', lastName: 'Doe' }]);
Teraz czas na Ciebie. Przy wykorzystaniu metod insertMany lub insertOne, bazując na powyższych przykładach, dodaj do kolekcji następujące dokumenty:
employees
{
firstName: 'John',
lastName: 'Doe',
department: 'IT'
},
{
firstName: 'Amanda',
lastName: 'Doe',
department: 'Marketing'
},
{
firstName: 'Jonathan',
lastName: 'Wilson',
department: 'IT'
},
{
firstName: 'Thomas',
lastName: 'Jefferson',
department: 'Testing'
},
{
firstName: 'Emma',
lastName: 'Cowell',
department: 'Testing'
}
departments
{ name: 'IT' },
{ name: 'Marketing' },
{ name: 'Testing' }
products
{
name: 'New Wave Festival',
client: 'MyMusicWave Corp.'
},
{
name: 'ImRich Banking official website',
client: 'ImRich LTD'
}
Uwaga!
MongoDB stara się być bardzo pomocne, do tego stopnia, że nawet jeśli pomylisz się i zrobisz literówkę w nazwie kolekcji, a więc zwyczajnie wybierzesz kolekcję nieistniejącą, to podobnie jak było to z wybraniem samej bazy danych, zostanie ona automatycznie utworzona.
Jeśli doszło do takiej sytuacji, zawsze możesz skorzystać z komendy drop. Potrafi ona usunąć wybraną (i np. błędnie nazwaną) kolekcję.
db.emplyes.drop()
Po wszystkich operacjach Twoja baza danych powinna wyglądać następująco:
Jak sprawdzić, czy rzeczywiście tak wygląda? Przyda się nam do tego metoda find.
Read
Drugą operacją, którą się zajmiemy, jest odczytywanie danych. Tutaj MongoDB daje nam bardzo szerokie możliwości, aby łatwo i precyzyjnie wybierać informacje nawet z ogromnych zbiorów.
Zacznijmy od początku. Interesują nas przede wszystkim dwie komendy:
find – służy do wyszukiwania wielu dokumentów,
findOne – szuka tylko jednego pasującego elementu.
W przypadku find dostaniemy tablicę z jednym elementem, wieloma lub pustą – w zależności od tego ile elementów spełni nasze warunki wyszukiwania. Można kojarzyć sobie rolę tej funkcji z filter, którą znasz z JS-a. Zadanie tamtej metody było bardzo podobne.
Stosując findOne, szukamy tylko jednego elementu. Jeśli baza zawiera jeden pasujący, to właśnie on zostanie zwrócony. W sytuacji, gdy będzie ich więcej, zwrócony zostanie pierwszy, który spełnia wymagania. Jeśli nie uda się znaleźć odpowiedniego elementu, metoda find zwróci po prostu null.
Jeśli chodzi o samo użycie metody, nie będzie się ono różniło od tego, co znamy już z insertOne czy insertMany. To wciąż ten sam koncept – db.<collection-name>.<method>.
Na przykład:
db.employees.findOne()
db.employees.find()
Pozostaje nam jeszcze kwestia warunków. Jeśli użyjemy findOne i find bez argumentu, to pierwsza metoda zwróci nam po prostu pierwszy dokument z kolekcji, a druga wszystkie. Przekonaj się o tym na własną rękę.
W ramach ćwiczenia uruchom po kolei:
db.employees.find()
db.departments.find()
db.products.find()
Konsola powinna zwrócić Ci całą zawartość każdej z kolekcji (tablicy).
Automatyczne id
Zapewne udało Ci się zauważyć, że zwrócone dane są trochę inne niż te, które dodawaliśmy. Każdy dokument otrzymał jedno nowe pole (atrybut), w postaci _id. Jest ono automatycznie nadawane dla każdego dokumentu jako unikalny identyfikator. To bardzo pożyteczna opcja, bo zazwyczaj i tak sami chcielibyśmy dodawać taki atrybut. MongoDB robi to za nas.
Opcja find(), która pokazuje nam wszystkie rekordy, jest przydatna, gdy np. chcemy zobaczyć całą zawartość danej kolekcji. Często zdarza się jednak, że potrzebujemy tylko jakąś część wyników, np. w aplikacji do zarządzania firmą, chcielibyśmy listować pracowników działu IT. Wtedy wolelibyśmy pobrać z bazy tylko dokumenty dotyczące tej grupy osób, a nie wszystkie. Na szczęście MongoDB daje nam takie możliwości. Co więcej, są one naprawdę szerokie. Nasze filtry mogą być bardzo precyzyjne, a wszystkie będziemy umieszczać jako atrybuty w argumencie funkcji.
Na przykład:
db.employees.find({
firstName: 'John',
department: { $ne: 'IT' }
});
Argumentem metody jest po prostu obiekt, w którym możemy ustalić jakie atrybuty chcemy sprawdzać i w jaki sposób. Powyższy przykład powinien szukać pracowników o imieniu John, pracujących w dziale innym niż IT ($ne to skrót od "negacji").
Poniżej omówimy dokładnie jak budować prostsze i trudniejsze warunki.
Konkretny warunek
Najprostsza sytuacja jest wtedy, kiedy chcemy wyszukać element, którego atrybuty mają jakąś konkretną wartość – nie większą czy mniejszą, albo "podobną", tylko dokładnie taką samą. W takim przypadku jako szukaną wartość atrybutu wystarczy wskazać konkretną informację. Robiliśmy to już wyżej, szukając pracowników o imieniu John.
Na przykład:
db.employees.find({ department: 'IT' });
zwróci nam tylko pracowników działu IT.
A taka instrukcja:
db.employees.findOne({ firstName: 'John', department: 'IT' });
będzie szukała pracownika, który nie tylko należy do działu IT, lecz również ma na imię John.
Przyznaj – to dość intuicyjne, prawda?
Mniej jasna sytuacja – używamy operatorów
Oczywiście nie zawsze sytuacja jest aż tak prosta. Często chcielibyśmy, aby te warunki były mniej konkretne, np. poszukujemy osoby poniżej 18 lat, czy też szukamy pracowników, którzy zarabiają od 2000 do 4000 tysięcy, albo – to, co już pokazaliśmy wcześniej – chcemy wyszukać dokumenty, których jakiś atrybut ma wartość inną niż wskazana przez nas (przykład z negacją).
W takich sytuacjach skorzystamy z szerokiej gamy dostarczonych przez MongoDB operatorów. Całą ich listę możesz znaleźć pod tym linkiem.
Poniżej przedstawiamy tylko niektóre z nich:
$eq – wartość atrybutu musi być dokładnie taka jak wskażemy, np. { age: { $eq: 21 }}. Oczywiście { age: 21 } też byłby tutaj poprawnym wyborem. $eq ma pewną przewagę nad zapisem bez operatora w kwestii bezpieczeństwa, ale nie będziemy poruszać tego tematu w tym module.
$gt – wartość atrybutu musi być większa niż podana, np. { age: { $gt: 18 }} zadziała jak warunek if(age > 18).
$gte – wartość atrybutu musi być większa lub równa podanej, np. age: { $gte: 18 } (warunek if(age >= 18)).
$in – wartość atrybutu musi być równa jednemu z możliwych wyborów. Możemy wskazać tablicę kilku opcji i jeśli chociaż jedna z nich odpowiada wartości atrybutu dokumentu, to będzie on zwrócony przy wyszukiwaniu, np. { hobby: { $in: ['sports', 'movies'] }} (to jak warunek if(hobby === 'sports' || hobby === 'movies').
$lt – wartość atrybutu musi być mniejsza niż podana, np. { age: { $lt: 18 }} (to jak warunek if(age < 18)).
$lte – wartość atrybutu musi być mniejsza lub równa podanej, np. { age: { $lte: 18 }} (to jak warunek if(age <= 18)).
$ne – wartość musi być inna niż podana (negacja), np. { firstName: { $ne: 'John' }} (to jak warunek if(firstName !== 'John')).
$nin – negacja $in. Wartość atrybutu nie może pasować do żadnego z podanych wyborów. Wskazujemy tablicę elementów i jeśli dany atrybut ma wartość pasującą do jednego z nich, to element nie będzie zwrócony.
Samo użycie operatorów pokazywaliśmy już wyżej w przykład z $ne. Dla jasności napiszmy to jednak jeszcze raz. W przypadku użycia operatorów, zamiast konkretnej wartości przy atrybucie, wstawiamy po prostu obiekt z operatorem.
Na przykład:
db.employees.find({
department: 'IT',
salary: { $gt: 2000 }
});
W tym przypadku znajdziemy pracowników, których atrybut department jest równy IT, ale pensja może być dowolna, o ile jest większa od 2000. Oczywiście ten przykład nie miałby sensu w naszej bazie danych, bo nie ma takiego pola jak salary.
db.employees.find({
department: { $nin: ['IT', 'Marketing'] }
});
Powyższa instrukcja wyszuka pracowników każdego innego działu niż IT czy Marketing.
Zwiększamy czytelność
Domyślnie find oraz findOne pokazuje rezultat w formie długiego ciągu bez żadnego formatowania. Jeśli chcesz, żeby MongoDB przedstawił Ci znalezione dane w trochę ładniejszy sposób, to wystarczy po pierwszej metodzie użyć jeszcze kolejnej – pretty. Zadba ona o odpowiednie wcięcia i podział na linie.
Wykorzystuje się ją w ten sposób:
db.employees.find().pretty();
W ramach treningu wykonaj następujące ćwiczenia.
Ćwiczenie 1
Wyszukaj w kolekcji employees wszystkich pracowników marketingu i działu IT, którzy nie mają na imię John.
Pokaż odpowiedź
Ukryj odpowiedź
db.employees.find({
department: { $in: ['IT', 'Marketing'] },
firstName: { $ne: 'John' }
});
Ćwiczenie 2
Znajdź wszystkich pracowników działu IT z kolekcji employees.
Pokaż odpowiedź
Ukryj odpowiedź
db.employees.find({
department: { $eq: 'IT' }
});
lub
db.employees.find({
department: 'IT'
});
Ćwiczenie 3
Znajdź wszystkie projekty (products), w których klient jest kimś innym niż MyMusicWave Corp..
Pokaż odpowiedź
Ukryj odpowiedź
db.products.find({
client: { $ne: 'MyMusicWave Corp.' }
});
Więcej warunków
Czy warunki mogą być jeszcze bardziej szczegółowe? Na przykład, gdybyśmy szukali osób w wieku z przedziału 18-40, lub chcieli sprawdzić pracowników, którzy np. zarabiają od 2000 do 4000 i od 6000 do 8000? Możemy to zrobić w MongoDB i wygląda to bardzo podobnie jak w zwykłych pętlach warunkowych. W takiej sytuacji używamy po prostu operatorów koniunkcji (&& – tutaj $and) oraz alternatywy (|| – tutaj $or).
Podobnie jak przy wcześniejszych instrukcjach, użycie $and oraz $or jest bardzo intuicyjne.
Składnia operatora $and wygląda tak:
{ $and: [<array-of-conditions>] }
W miejscu <array-of-conditions> możemy wstawiać tablicę tylu warunków, ilu tylko chcemy.
Na przykład wyszukiwanie osób, które mają więcej niż 18 lat, a mniej niż 40, mogłoby wyglądać następująco:
db.persons.find({ $and: [{ age: { $gt: 18 } }, { age: { $lt: 40 }}] });
Taki zapis możemy rozumieć jak rozkaz: znajdź osoby, których wiek (age) spełnia dwa warunki – jest większy niż 18 i mniejszy niż 40. W zwykłej pętli warunkowej wyglądałoby to mniej więcej tak: if(age > 18 && age < 40).
Zapis bez $and
Warto dodać, że często istnieje również możliwość bezpośredniego użycia kilku operatorów dla danej właściwości, co dałoby nam taki sam efekt:
db.persons.find({ age: { $gt: 18, $lt: 40 }});
Jednak $and ma jedną ważną zaletę, której taki zapis nam nie daje – możemy kilkukrotnie warunkować jakiś jeden atrybut. Na przykład taki kod:
db.persons.find({ name: { $ne: 'John' }, name: { $ne: 'Amanda' }});
sprawi, że dla MongoDB istotny będzie tylko ostatni warunek, a więc znajdziemy wszystkie osoby, których imię jest inne niż Amanda. Natomiast
db.persons.find({ $and: [{ name: { $ne: 'John' }}, { name: { $ne: 'Amanda' }}] });
będzie pilnować, aby szukało nam osób, które mają imię inne od Johna, ale i Amandy.
Często $and używa się dla samej czytelności przekazu, ale warto stosować go przede wszystkim wtedy, kiedy właśnie mamy taką sytuację jak wyżej.
Ćwiczenie
W ramach ćwiczenia zajmijmy się taką przykładową kolekcją danych:
db.persons
[
{ name: 'John Doe', age: 20, salary: 5000 },
{ name: 'Amanda Doe', age: 20, salary: 3000 },
{ name: 'Thomas Jefferson', age: 50, salary: 3000 },
{ name: 'William Haze', age: 18, salary: 2000 }
]
Zastanów się, jak wyglądałaby instrukcja, która znajdowałaby wszystkie osoby będące w przedziale wiekowym od 20 do 50 lat i zarabiające od 2500 do 4000.
Pokaż odpowiedź
Ukryj odpowiedź
db.persons.find({
$and: [{
age: { $gt: 20, $lt: 50 }
},
{
salary: { $gt: 2500, $lt: 4000 }
}]
});
Całkiem logiczne, prawda? ;)
Podobnie możemy wykorzystywać $or.
{ $or: [<array-of-conditions>] }
$or to oczywiście alternatywa, więc wystarczy, aby tylko jedna opcja pasowała.
Przykładowo wyszukiwanie osób, które zarabiają mniej niż 2500 lub więcej niż 4000, mogłoby wyglądać tak:
db.persons.find({
$or: [
{ salary: { $lt: 2500 } }, { salary: { $gt: 4000 } }
]
});
Możemy to rozumieć jako rozkaz: znajdź takie osoby, których pensja (salary) jest mniejsza od 2500 albo większa od 4000.
Ćwiczenie
Spróbuj zastanowić się, jak wyglądałaby analogiczna instrukcja dla tej samej kolekcji co w poprzednim ćwiczeniu (db.persons), która znajdowałaby wszystkie osoby będące w przedziale wiekowym powyżej 45 lub poniżej 20 lat i zarabiające od 1000 do 3000.
Pokaż odpowiedź
Ukryj odpowiedź
db.persons.find({
$or: [
{ age: { $lt: 20 } }, { age: { $gt: 45 } }
],
$and: [
{ salary: { $gt: 1000 } },
{ salary: { $lt: 3000 } }
]
});
Podobnie jak w samym JS-ie możemy ze sobą łączyć $and i $or, aby stworzyć bardziej zaawansowane warunki.
Na przykład:
db.persons.find({
$or: [
{ $and: [
{ age: { $gt: 20 } },
{ age: { $lt: 50 } }
]},
{ age: { $eq: 18 } }
]
});
Ten kod wyszukałby osoby, których wiek mieści się w przedziale 20-50, albo mają równo 18 lat.
Możemy również używać $and i $or szerzej, na przykład:
db.persons.find({
$or: [
{
age: 18,
salary: { $gt: 3000 }
},
{
age: { $gt: 25 }
}
]
});
Powyższy kod wyszukiwałby osoby, które albo mają 18 lat i zarabiają więcej niż 3000, albo mają więcej niż 25 lat.
Oczywiście tego typu przykłady, mimo że wciąż w miarę intuicyjne, mogą być dla Ciebie zbyt skomplikowane. Nie obawiaj się, nieczęsto będziemy potrzebować aż tak dokładnych warunków, aby wybrać potrzebne dane. Chcieliśmy Ci po prostu pokazać jakie możliwości drzemią w metodach find i findOne. Przyznaj, że takie wybieranie jest o wiele lepsze niż "przechodzenie" po zwykłej tablicy za pomocą filter czy forEach, co robiliśmy w czystym JS-ie.
Update
Czas na aktualizację danych. MongoDB oferuje tutaj dwie metody:
updateMany – służy do aktualizacji wielu dokumentów,
updateOne – służy do aktualizacji tylko jednego pasującego dokumentu.
Pierwsza (updateMany) ma za zadanie znaleźć wszystkie elementy pasujące do warunku, który ustalimy i zmodyfikować je zgodnie z naszym życzeniem. Może to być jeden pasujący element, dwa, albo nawet wszystkie z danej kolekcji.
Druga (updateOne) szuka tylko jednego elementu pasującego do założonego warunku i tylko jego aktualizuje. Jeśli dokumentów pasujących jest więcej, to metoda ta zmodyfikuje po prostu pierwszy, który znajdzie. Jeśli żaden dokument nie pasuje, to zwyczajnie nic nie zostanie zrobione.
Ich składnia, podobnie jak w przypadku wcześniejszych metod, jest bardzo intuicyjna. Wygląda to następująco:
db.<collection-name>.updateMany(<condition>, <data-to-update>)
db.<collection-name>.updateOne(<condition>, <data-to-update>)
Pierwszy parametr będzie wyglądał tak samo, jak w przypadku find. Możemy więc ustalać w nim, że szukamy dokumentów, których np. name to John ({ name: 'John' }), albo wiek jest większy od 18 { age: { $gt: 18 }}. Możemy używać prostych warunków lub korzystać z wszelkich znanych Ci już operatorów. To po prostu parametr, który mówi, jakich elementów szukamy.
Drugi parametr ma za to ustalić, w jaki sposób chcemy zmodyfikować te dokumenty. Przyjmuje on następującą składnię:
{ $set: { <attrs-to-update>} }
Jest to obiekt z atrybutem o nazwie $set. Wartością tego atrybutu powinien być nowy obiekt, który będzie wskazywać jakie właściwości pasujących dokumentów powinny być zmodyfikowane (nie musimy bowiem aktualizować koniecznie całego dokumentu) i jaka ma być ich nowa wartość.
Spójrzmy na przykład:
db.persons.updateMany({ salary: { $gt: 3000 }}, { $set: { salary: 2500 }});
Powyższy kod znalazłby wszystkie osoby zarabiające powyżej 3000 i zmniejszył ich pensję do 2500. Taka instrukcja brzmiałaby bowiem jak rozkaz: znajdź wszystkie elementy, których atrybut salary ma wartość większą od 3000 i zmniejsz ich atrybut salary do 2500.
Oczywiście nie musimy modyfikować akurat tego atrybutu, po którym wyszukiwaliśmy pasujące rekordy.
Na przykład:
db.persons.update({ age: { $gt: 18 }}, { $set: { salary: 5000 }});
Tutaj szukalibyśmy pierwszej osoby z kolekcji, która ma więcej niż 18 lat i ustawialibyśmy jej atrybut salary (a więc pensję) na 5000.
Naturalnie liczba atrybutów, po których szukamy dokumentów, może być dowolna, podobnie zresztą jak w find. Dowolna może być również liczba modyfikacji, które chcemy do nich wprowadzić.
W tym momencie może pojawić się jeszcze jedno pytanie – a co jeśli w $set wstawilibyśmy atrybut, który w dokumencie nie istnieje? Cóż, znasz już trochę MongoDB, więc możesz się domyślać. W takiej sytuacji silnik zwyczajnie dodałby nowy atrybut do dokumentu/dokumentów.
Ćwiczenie
W ramach ćwiczenia przeszukamy taką przykładową kolekcję danych:
db.persons
[
{ name: 'John Doe', age: 20, salary: 5000 },
{ name: 'Amanda Doe', age: 20, salary: 3000 },
{ name: 'Thomas Jefferson', age: 50, salary: 3000 },
{ name: 'William Haze', age: 18, salary: 2000 }
]
Zastanów się, jak wyglądałaby instrukcja, która znajdowałaby wszystkie osoby będące w przedziale wiekowym od 18 do 30 lat i zarabiające od 2500 do 4000, a następnie zmieniała ich pensje na sztywne 3000.
Pokaż odpowiedź
Ukryj odpowiedź
db.persons.updateMany({
$and: [
{ age: { $gt: 18, $lt: 30 } },
{ salary: { $gt: 2500, $lt: 4000 } }
]
},
{
$set: { salary: 3000 }
});
Remove
Pozostała nam ostatnia funkcjonalność do omówienia – usuwanie danych. MongoDB oferuje nam tutaj dwie możliwości:
deleteOne – służy do usuwania jednego dokumentu,
deleteMany - służy do usuwania wielu dokumentów.
Składnia nie będzie tutaj dla Ciebie żadną nowością:
db.<collection-name>.deleteOne(<condition>)
db.<collection-name>.deleteMany(<condition>)
W miejscu <condition> wstawiamy po prostu warunek, po którym chcemy szukać pasujących dokumentów. Ponownie, wygląda to dokładnie tak samo, jak w find i pierwszym parametrze updateOne/updateMany. Może to być prosty warunek (np. { age: 18 }) albo bardziej skomplikowany z operatorami. Na dobrą sprawę, składnia jest wręcz identyczna jak find, z tym, że find wyszukuje pasujące elementy i tylko je zwraca, a tutaj nie tylko ich szukamy, ale również usuwamy.
Zauważ, że podobny warunek możemy zapisać także bez $and. Na przykład:
db.persons.deleteMany({ age: { $gt: 18, $lt: 30 } });
Powyższy kod znalazłby i usunął wszystkie osoby w przedziale wiekowym 18-30.
Drugi przykład:
db.persons.deleteOne({ age: 18 });
Tutaj usunęlibyśmy pierwszą osobę z kolekcji, której wiek byłby równy 18.
Podsumowanie
Zanim poznaliśmy MongoDB, mógł Ci się jawić jako kolejne skomplikowane narzędzie, które pewnie wspomoże pracę nad dużymi zbiorami danych, ale też dołoży sporą dawkę wiedzy do przyswojenia.
Mamy nadzieję, że teraz jego odbiór jest już trochę inny.
Mimo nowej wiedzy i potrzeby zapamiętania kilku metod nie można odmówić MongoDB intuicyjności. Bardzo podobny sposób przechowywania danych do tego, który znamy już z JS-a oraz prosta konstrukcja metod powoduje, że z MongoDB pracuje się podobnie jak przy użyciu zwykłych tablic, a może nawet prościej.
Oczywiście trochę nauki jeszcze przed nami. Musimy dowiedzieć się, jak korzystać z MongoDB w aplikacjach Node.js, poznamy też Mongoose i jej ideę modeli danych. Początek sugeruje jednak, że nie będzie to aż tak trudne, jak mogło się wydawać.
Zarówno MongoDB, jak i Mongoose, to bardzo intuicyjne technologie, które po tym module z pewnością umieścisz w swoim programistycznym arsenale.